package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.codec.EncodeException;
import io.kiki.stack.http.feign.codec.Encoder;
import io.kiki.stack.http.feign.template.UriUtils;

import java.util.*;

import static io.kiki.stack.http.feign.Util.checkArgument;
import static io.kiki.stack.http.feign.Util.checkNotNull;

final class RequestTemplateFactoryResolver {
    private final Encoder encoder;
    private final QueryMapEncoder queryMapEncoder;

    RequestTemplateFactoryResolver(Encoder encoder, QueryMapEncoder queryMapEncoder) {
        this.encoder = checkNotNull(encoder, "encoder");
        this.queryMapEncoder = checkNotNull(queryMapEncoder, "queryMapEncoder");
    }

    public RequestTemplateFactory resolve(Target<?> target, MethodMetadata md) {
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
            return new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else
            if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
                return new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
            } else {
                return new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
            }
    }

    static class BuildTemplateByResolvingArgs implements RequestTemplateFactory {

        protected final MethodMetadata methodMetadata;
        protected final Target<?> target;
        private final QueryMapEncoder queryMapEncoder;
        private final Map<Integer, Param.Expander> indexToExpander = new LinkedHashMap<Integer, Param.Expander>();

        BuildTemplateByResolvingArgs(MethodMetadata methodMetadata, QueryMapEncoder queryMapEncoder, Target target) {
            this.methodMetadata = methodMetadata;
            this.target = target;
            this.queryMapEncoder = queryMapEncoder;
            if (methodMetadata.indexToExpander() != null) {
                indexToExpander.putAll(methodMetadata.indexToExpander());
                return;
            }
            if (methodMetadata.indexToExpanderClass().isEmpty()) {
                return;
            }
            for (Map.Entry<Integer, Class<? extends Param.Expander>> indexToExpanderClass : methodMetadata.indexToExpanderClass().entrySet()) {
                try {
                    indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
                } catch (InstantiationException e) {
                    throw new IllegalStateException(e);
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
            }
        }

        @Override
        public RequestTemplate create(Object[] argv) {
            RequestTemplate requestTemplate1 = RequestTemplate.from(methodMetadata.template());
            requestTemplate1.feignTarget(target);
            if (methodMetadata.urlIndex() != null) {
                int urlIndex = methodMetadata.urlIndex();
                checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
                requestTemplate1.target(String.valueOf(argv[urlIndex]));
            }
            Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
            for (Map.Entry<Integer, Collection<String>> entry : methodMetadata.indexToName().entrySet()) {
                int i = entry.getKey();
                Object value = argv[entry.getKey()];
                if (value != null) { // Null values are skipped.
                    if (indexToExpander.containsKey(i)) {
                        value = expandElements(indexToExpander.get(i), value);
                    }
                    for (String name : entry.getValue()) {
                        varBuilder.put(name, value);
                    }
                }
            }

            RequestTemplate requestTemplate = resolve(argv, requestTemplate1, varBuilder);
            if (methodMetadata.queryMapIndex() != null) {
                // add query map parameters after initial resolve so that they take
                // precedence over any predefined values
                Object value = argv[methodMetadata.queryMapIndex()];
                Map<String, Object> queryMap = toQueryMap(value, methodMetadata.queryMapEncoder());
                requestTemplate = addQueryMapQueryParameters(queryMap, requestTemplate);
            }

            if (methodMetadata.headerMapIndex() != null) {
                // add header map parameters for a resolution of the user pojo object
                Object value = argv[methodMetadata.headerMapIndex()];
                Map<String, Object> headerMap = toQueryMap(value, methodMetadata.queryMapEncoder());
                requestTemplate = addHeaderMapHeaders(headerMap, requestTemplate);
            }

            return requestTemplate;
        }

        private Map<String, Object> toQueryMap(Object value, QueryMapEncoder queryMapEncoder) {
            if (value instanceof Map) {
                return (Map<String, Object>) value;
            }
            try {
                // encode with @QueryMap annotation if exists otherwise with the one from this resolver
                return queryMapEncoder != null ? queryMapEncoder.encode(value) : this.queryMapEncoder.encode(value);
            } catch (EncodeException e) {
                throw new IllegalStateException(e);
            }
        }

        private Object expandElements(Param.Expander expander, Object value) {
            if (value instanceof Iterable) {
                return expandIterable(expander, (Iterable) value);
            }
            return expander.expand(value);
        }

        private List<String> expandIterable(Param.Expander expander, Iterable value) {
            List<String> list = new ArrayList<String>();
            for (Object element : value) {
                if (element != null) {
                    list.add(expander.expand(element));
                }
            }
            return list;
        }

        @SuppressWarnings("unchecked")
        private RequestTemplate addHeaderMapHeaders(Map<String, Object> headerMap, RequestTemplate requestTemplate) {
            for (Map.Entry<String, Object> entry : headerMap.entrySet()) {
                Collection<String> list = new ArrayList<String>();

                Object currValue = entry.getValue();
                if (currValue instanceof Iterable<?>) {
                    Iterator<?> iterator = ((Iterable<?>) currValue).iterator();
                    while (iterator.hasNext()) {
                        Object nextObject = iterator.next();
                        list.add(nextObject == null ? null : nextObject.toString());
                    }
                } else {
                    list.add(currValue == null ? null : currValue.toString());
                }

                requestTemplate.header(entry.getKey(), list);
            }
            return requestTemplate;
        }

        @SuppressWarnings("unchecked")
        private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate requestTemplate) {
            for (Map.Entry<String, Object> entry : queryMap.entrySet()) {
                Collection<String> list = new ArrayList<String>();

                Object currValue = entry.getValue();
                if (currValue instanceof Iterable<?>) {
                    Iterator<?> iterator = ((Iterable<?>) currValue).iterator();
                    while (iterator.hasNext()) {
                        Object nextObject = iterator.next();
                        list.add(nextObject == null ? null : UriUtils.encode(nextObject.toString()));
                    }
                } else
                    if (currValue instanceof Object[]) {
                        for (Object value : (Object[]) currValue) {
                            list.add(value == null ? null : UriUtils.encode(value.toString()));
                        }
                    } else {
                        if (currValue != null) {
                            list.add(UriUtils.encode(currValue.toString()));
                        }
                    }

                if (list.size() > 0) {
                    requestTemplate.query(UriUtils.encode(entry.getKey()), list);
                }
            }
            return requestTemplate;
        }

        protected RequestTemplate resolve(Object[] argv, RequestTemplate requestTemplate, Map<String, Object> variables) {
            return requestTemplate.resolve(variables);
        }
    }

    static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

        private final Encoder encoder;

        BuildFormEncodedTemplateFromArgs(MethodMetadata methodMetadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) {
            super(methodMetadata, queryMapEncoder, target);
            this.encoder = encoder;
        }

        @Override
        protected RequestTemplate resolve(Object[] argv, RequestTemplate requestTemplate, Map<String, Object> variables) {
            Map<String, Object> map = new LinkedHashMap<String, Object>();
            for (Map.Entry<String, Object> entry : variables.entrySet()) {
                if (methodMetadata.formParams().contains(entry.getKey())) {
                    map.put(entry.getKey(), entry.getValue());
                }
            }
            try {
                encoder.encode(map, Encoder.MAP_STRING_WILDCARD, requestTemplate);
            } catch (EncodeException e) {
                throw e;
            } catch (RuntimeException e) {
                throw new EncodeException(e.getMessage(), e);
            }
            return super.resolve(argv, requestTemplate, variables);
        }
    }

    static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

        private final Encoder encoder;

        BuildEncodedTemplateFromArgs(MethodMetadata methodMetadata, Encoder encoder, QueryMapEncoder queryMapEncoder, Target target) {
            super(methodMetadata, queryMapEncoder, target);
            this.encoder = encoder;
        }

        @Override
        protected RequestTemplate resolve(Object[] argv, RequestTemplate requestTemplate, Map<String, Object> variables) {

            boolean alwaysEncodeBody = requestTemplate.methodMetadata().alwaysEncodeBody();

            Object body = null;
            if (!alwaysEncodeBody) {
                body = argv[methodMetadata.bodyIndex()];
                if (requestTemplate.methodMetadata().isBodyRequired()) {
                    checkArgument(body != null, "Body parameter %s was null", methodMetadata.bodyIndex());
                }
            }

            try {
                if (alwaysEncodeBody) {
                    body = argv == null ? new Object[0] : argv;
                    encoder.encode(body, Object[].class, requestTemplate);
                } else {
                    encoder.encode(body, methodMetadata.bodyType(), requestTemplate);
                }
            } catch (EncodeException e) {
                throw e;
            } catch (RuntimeException e) {
                throw new EncodeException(e.getMessage(), e);
            }
            return super.resolve(argv, requestTemplate, variables);
        }
    }
}
